﻿/*
 * Copyright (c) 2009, Thiago M. Sayao <thiago.sayao@gmail.com>
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 *  * Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *  * Neither the name of the author nor the names of its contributors may be 
 *    used to endorse or promote products derived from this software without 
 *    specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 * POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Linq;
using System.Web;
using System.ServiceModel.Syndication;
using System.IO;
using System.Globalization;
using System.Xml;
using System.Text;
using System.Collections.Generic;
using System.Threading;
using System.Transactions;
using DotNetBurner.Model;
using DotNetBurner.Core;
using DotNetBurner.Web;
using Resources;

namespace DotNetBurner
{
    public class PlanetManager
    {
        private readonly string _feedsFile;
        private readonly string _feedCacheFile;
        private readonly string _planetTplFile;
        private readonly string _planetFile;
        private static readonly string HtmlCacheKey = "planet-html";
        private static readonly string FeedCacheKey = "planet-feed";

        readonly Repository _repo;
        List<Feed> _feedsToUpdate;
        bool _anyFeedChanged;

        internal struct FeedItem
        {
            public Feed Feed
            {
                get;
                set;
            }

            public string Title
            {
                get;
                set;
            }

            public string Content
            {
                get;
                set;
            }

            public string Link
            {
                get;
                set;
            }

            public DateTime PublishedDateTime
            {
                get;
                set;
            }
        }

        //Yes, this looks a mess, but it updates asynchronously and on-demand :)
        public PlanetManager(Repository repo)
        {
            string basePath = HttpRuntime.AppDomainAppPath;

            _feedsFile = Path.Combine(basePath, @"Cache\Feeds");
            _feedCacheFile = Path.Combine(basePath, @"Cache\Feeds\feed-cache-{0}.xml".FormatWith(SiteSettings.Settings.Name));
            _planetTplFile = Path.Combine(basePath, @"App_Data\feed-item.tpl");
            _planetFile = Path.Combine(basePath, @"Cache\Feeds\planet-{0}.html".FormatWith(SiteSettings.Settings.Name));

            _repo = repo;
        }

        public string GetPlanetHtml()
        {
            return GetData(HtmlCacheKey, _planetFile);
        }

        public string GetPlanetFeed()
        {
            return GetData(FeedCacheKey, _feedCacheFile);
        }

        public string GetData(string cacheKey, string file)
        {
            string data;

            //update feeds on the background;
            UpdateFeeds();

            if (!HttpRuntime.Cache.TryGet(cacheKey, out data))
            {
                if (File.Exists(file))
                    data = File.ReadAllText(file, Encoding.UTF8);
                else
                    data = Strings.WaitWhileFeedsUpdate;

                HttpRuntime.Cache.CacheData(cacheKey, "Planet", data);
            }

            return data;
        }

        public void UpdateFeeds()
        {
            DateTime lastUpdated = DateTime.UtcNow.AddMinutes(-PlanetSettings.Settings.UpdateMinutesInterval);
            _feedsToUpdate = new List<Feed>();

            using (var tran = new TransactionScope(TransactionScopeOption.RequiresNew, TimeSpan.FromSeconds(10)))
            {
                var feeds = _repo.GetFeedsToUpdate(lastUpdated);

                //only update the feeds that needs update
                foreach (Feed feed in feeds)
                {
                    _repo.SetFeedLastUpdated(feed.FeedId, DateTime.UtcNow);
                    _feedsToUpdate.Add(feed);
                }

                tran.Complete();
            }

            //this lock prevents the collection to be modified during the loop
            lock (_feedsToUpdate)
            {
                //the update happens asynchronously (does not block)
                foreach (var feed in _feedsToUpdate)
                    WebUtility.BeginGetDataFromUrl(EndGetFeed, feed, feed.Url, 8 * 1000, true, feed.LastUpdated);
            }

        }

        void EndGetFeed(IAsyncResult result)
        {
            try
            {
                HttpRequestState reqState = (HttpRequestState)result.AsyncState;
                Feed feed = (Feed)reqState.State;

                //check if the feed changed (WebUtility doesnt download if it didn't change)
                if (reqState.ChangedSinceLastDownload)
                {
                    string content = WebUtility.EndGetDataFromUrl(result);
                    File.WriteAllText(Path.Combine(_feedsFile,
                                                   "{0}.xml".FormatWith(feed.Url.ConvertToFileName())),
                                      content.Trim(), reqState.Encoding);

                    _anyFeedChanged = true;
                }

                lock (_feedsToUpdate)
                {
                    _feedsToUpdate.Remove(feed);
                }

                if (_anyFeedChanged && _feedsToUpdate.Count == 0)
                {
                    //if any feed changed and this is the last feed updated, update the cache
                    UpdateCache();
                }
            }
            catch (Exception exception)
            {
                Log.Exception(exception);
            }
        }

        void UpdateCache()
        {
            //template for the _feeds
            string itemTpl = File.ReadAllText(_planetTplFile, Encoding.UTF8);

            SyndicationFeed newFeed = new SyndicationFeed();
            newFeed.Generator = SiteSettings.Settings.Name;
            newFeed.Title = new TextSyndicationContent(SiteSettings.Settings.Title);
            newFeed.BaseUri = new Uri(SiteSettings.Settings.Address);

            List<SyndicationItem> items = new List<SyndicationItem>();
            List<FeedItem> feedItems = new List<FeedItem>();

            int totalCount = 0;

            //read all _feeds
            foreach (Feed feed in _repo.GetFeeds())
            {
                if (totalCount == PlanetSettings.Settings.MaxTotalFeeds)
                    break;

                string file = Path.Combine(_feedsFile, "{0}.xml".FormatWith(feed.Url.ConvertToFileName()));

                try
                {
                    FileInfo fileInfo = new FileInfo(file);
                    if (fileInfo.Exists && fileInfo.Length > 0)
                    {
                        DateTime maxDate = DateTime.UtcNow.AddDays(-PlanetSettings.Settings.MaxFeedDaysOld);

                        XmlReader reader = XmlReader.Create(file);
                        // Load the feed into a SyndicationFeed 
                        SyndicationFeed syndicationFeed = SyndicationFeed.Load(reader);

                        int count = 0;

                        foreach (SyndicationItem syndicationItem
                            in syndicationFeed.Items.OrderByDescending(i => i.PublishDate))
                        {
                            if (syndicationItem.PublishDate < maxDate)
                                continue;

                            if (count == PlanetSettings.Settings.MaxFeedsPerFeed)
                                break;

                            //post link
                            string url = String.Empty;

                            if (syndicationItem.Links.Count > 0)
                            {
                                if (syndicationItem.Links.Count == 1)
                                    url = syndicationItem.Links[0].Uri.OriginalString;
                                else
                                {
                                    foreach (SyndicationLink link in syndicationItem.Links)
                                        if (link.RelationshipType.Equals("alternate",
                                                                         StringComparison.OrdinalIgnoreCase))
                                        {
                                            url = link.Uri.OriginalString;
                                            break;
                                        }
                                }
                            }

                            string proxyFreeUrl = url;

                            try
                            {
                                //find the original link
                                XmlReader xmlReader = syndicationItem.ElementExtensions.GetReaderAtElementExtensions();

                                while (xmlReader.Read())
                                {
                                    //aha! got you feedburner!
                                    if (xmlReader.Name == "feedburner:origLink")
                                    {
                                        proxyFreeUrl = xmlReader.ReadElementString();
                                        break;
                                    }
                                }
                            }
                            catch (Exception) { }

                            string summary;

                            if (syndicationItem.Summary != null)
                                summary = GetSummary(syndicationItem.Summary.Text);
                            else
                                summary = GetSummary(((TextSyndicationContent)syndicationItem.Content).Text);

                            string title = syndicationItem.Title.Text;

                            //Add link button to the _feeds
                            string voteButton =
                                "<br /><br />{0}<br /><br />".FormatWith(SwissKnife.GetVoteButtonFor(proxyFreeUrl));

                            string itemSummary = String.Concat(summary, "&nbsp",
                                                               String.Format(
                                                                   "<a href=\"{0}\" title=\"{1}\" target=\"_blank\">{2}</a>",
                                                                   url, syndicationItem.Title.Text, Strings.More),
                                                               voteButton);

                            //Create feed items to update planet cache
                            feedItems.Add(new FeedItem
                            {
                                Title = title,
                                PublishedDateTime = syndicationItem.PublishDate.DateTime,
                                Content = itemSummary,
                                Link = url,
                                Feed = feed
                            });

                            //new title & new summary
                            syndicationItem.Summary = SyndicationContent.CreateHtmlContent(itemSummary);
                            syndicationItem.Content = null; 
                            syndicationItem.Title   = new TextSyndicationContent(title);


                            //remove unwanted extensions
                            var contentExt = syndicationItem.ElementExtensions
                                             .FirstOrDefault(f => f.OuterName.ToLowerInvariant() == "content");

                            if (contentExt != default(SyndicationElementExtension))
                                syndicationItem.ElementExtensions.Remove(contentExt);


                            //Add to the collection of _feeds
                            items.Add(syndicationItem);

                            count++;
                            totalCount++;
                        }

                        if (reader != null)
                            reader.Close();
                    }

                    //reoder to save (some feed readers do not reorder by date - also feedburner)
                    newFeed.Items = items.OrderByDescending(i => i.PublishDate);
                }
                catch (Exception exception)
                {
                    Log.Error("Loading feed: {0} failed.".FormatWith(feed.Name));
                    Log.Exception(exception);
                }
            }

            using (XmlTextWriter writer = new XmlTextWriter(_feedCacheFile, Encoding.UTF8))
            {
                newFeed.SaveAsAtom10(writer);
            }

            //cache the feed
            HttpRuntime.Cache.CacheData(FeedCacheKey, "Planet", File.ReadAllText(_feedCacheFile));

            UpdatePlanet(feedItems);
        }

        //update the template file
        void UpdatePlanet(List<FeedItem> feedItems)
        {
            string itemTpl = File.ReadAllText(_planetTplFile, Encoding.UTF8);

            var items = (from i in feedItems
                         orderby i.PublishedDateTime descending, i.Title
                         select i).Take (PlanetSettings.Settings.MaxTotalFeeds);

            StringBuilder b = new StringBuilder();

            string date = String.Empty;
            CultureInfo culture = new CultureInfo(SiteSettings.Settings.Language);

            foreach (FeedItem item in items)
            {
                string currentDate = item.PublishedDateTime.ToString("d", culture);

                if (date != currentDate)
                {
                    b.AppendFormat("<div class=\"tab\"><h2>{0}</h2></div>", currentDate);

                    date = currentDate;
                }

                string str = itemTpl.Replace("@@ENTRY_HTML@@", item.Content)
                    .Replace("@@ENTRY_TITLE@@", item.Title)
                    .Replace("@@ENTRY_PERSON@@", item.Feed.Name)
                    .Replace("@@ENTRY_PERSON_URL@@", item.Feed.SiteUrl)
                    .Replace("@@ENTRY_LINK@@", item.Link)
                    .Replace("@@ENTRY_DATE@@", item.PublishedDateTime.ToString("g", culture))
                    .Replace("@@ENTRY_PERSON_GRAVATAR@@", item.Feed.Email.EncryptMD5());

                b.Append(str);
            }

            string planetHtml = b.ToString();

            File.WriteAllText(_planetFile, planetHtml, Encoding.UTF8);

            //Cache it
            HttpRuntime.Cache.CacheData(HtmlCacheKey, "Planet", planetHtml);
        }

        static string GetSummary(string description)
        {
            return HtmlUtil.ConvertToText(description).GetSummary(512, 1024).HtmlEncode();
        }
    }
}
